package com;

import javax.swing.*;
import com.trusfort.redis.RedisServiceUtil;
import com.util.FileUtils;
import org.jfree.chart.ChartFactory;
import org.jfree.chart.ChartFrame;
import org.jfree.chart.JFreeChart;
import org.jfree.data.general.DefaultPieDataset;
import redis.clients.jedis.exceptions.JedisConnectionException;
import redis.clients.jedis.resps.ScanResult;

import java.awt.BorderLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.File;
import java.io.StringWriter;
import java.io.PrintWriter;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicLong;

public class RedisMemoryStat {
    // GUI Components
    private static JTextField txtRedisAddress;
    private static JPasswordField txtRedisPassword;
    private static JTextField txtSavePath;
    private static JTextArea txtLog;
    private static JTextArea txtResult;
    private static JComboBox<Integer> dbComboBox;
    private static JComboBox<String> modeComboBox;
    private static JTextField txtKeyPrefix;
    private static JTextField txtDefaultKeys;
    private static JFrame jf;
    private static JProgressBar progressBar;

    // Constants
    private static final String[] DEFAULT_REDIS_KEYS = {
        "XDID::adid", "XDID::aimei", "XDID::abmac", "XDID::amac", "XDID::aaid", 
        "XDID::abid", "XDID::absn", "XDID::afpid", "XDID::asn", "XDID::acid", 
        "XDID::afit", "XDID::afdidrand", "XDID::fshash", "XDID::ext_id1", 
        "XDID::_did4q_", "XDID::_did4q_", "mtd:"
    };
    
    private static final String[] EXCLUDES = {"mtd", "XDID", "qlog"};
    private static final int MIN_SCAN_COUNT = 100;
    private static final int MAX_SCAN_COUNT = 5000;
    
    // Shared resources
    private static final ExecutorService executorService = 
        Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors() * 2);
    
    // Instance variables
    private String[] redisKeys = DEFAULT_REDIS_KEYS;
    private final Map<String, Map<String, Long>> data = new ConcurrentHashMap<>();
    private final AtomicLong totalKeys = new AtomicLong(0);
    private final AtomicLong estimatedTotalKeys = new AtomicLong(0);
    private int currentScanCount = 1000;
    private static volatile boolean isRunning = false;
    private static boolean headlessMode = false;

    // Logging methods
    public static void logInfo(String msg) {
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        String cur = sdf.format(new Date());
        String sanitizedMsg = sanitizeMessage(msg);
        String logMessage = cur + " - " + sanitizedMsg;
        
        if (!headlessMode) {
            SwingUtilities.invokeLater(() -> {
                txtLog.append(logMessage + "\n");
                txtLog.setCaretPosition(txtLog.getDocument().getLength());
            });
        } else {
            System.out.println(logMessage);
        }
        
        FileUtils.write(txtSavePath.getText() + "/logs/info.txt", 
            cur + " [INFO] " + sanitizedMsg + "\n");
        rotateLogs();
    }

    public static void logError(String msg, Throwable e) {
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        String cur = sdf.format(new Date());
        final String errorMsg;
        
        if (e != null) {
            errorMsg = cur + " - [ERROR] " + sanitizeMessage(msg) + " - " + 
                      e.getClass().getSimpleName() + ": " + sanitizeMessage(e.getMessage());
        } else {
            errorMsg = cur + " - [ERROR] " + sanitizeMessage(msg);
        }
        
        if (!headlessMode) {
            SwingUtilities.invokeLater(() -> {
                txtLog.append(errorMsg + "\n");
                txtLog.setCaretPosition(txtLog.getDocument().getLength());
            });
        } else {
            System.err.println(errorMsg);
            if (e != null) {
                e.printStackTrace();
            }
        }
        
        FileUtils.write(txtSavePath.getText() + "/logs/error.txt", errorMsg + "\n");
        
        if (e != null) {
            StringWriter sw = new StringWriter();
            e.printStackTrace(new PrintWriter(sw));
            FileUtils.write(txtSavePath.getText() + "/logs/error_stack.txt", 
                cur + " [ERROR] " + sanitizeMessage(msg) + "\n" + sw.toString() + "\n");
        }
        rotateLogs();
    }
    
    private static String sanitizeMessage(String msg) {
        if (msg == null) return "";
        return msg;
    }
    
    private static void rotateLogs() {
        File logDir = new File(txtSavePath.getText() + "/logs");
        if (!logDir.exists()) return;
        
        File[] logFiles = logDir.listFiles((dir, name) -> name.endsWith(".txt"));
        if (logFiles != null && logFiles.length > 5) {
            Arrays.sort(logFiles, Comparator.comparingLong(File::lastModified));
            for (int i = 0; i < logFiles.length - 5; i++) {
                logFiles[i].delete();
            }
        }
    }

    // Key processing methods
    private String getKeyPrefix(String key) {
        for (String keyPrefix : redisKeys) {
            if (key.startsWith(keyPrefix)) {
                return keyPrefix;
            }
        }
        return "其他";
    }

    private boolean isExcluded(String key) {
        for (String exclude : EXCLUDES) {
            if (key.startsWith(exclude)) {
                return true;
            }
        }
        return false;
    }
    
    private void processKeysBatch(int db, List<String> keys) {
        try {
//            List<Object> memoryUsage = RedisServiceUtil.getMemoryUsage(db, keys);
            
            for (int i = 0; i < keys.size(); i++) {
                String key = keys.get(i);
//                if (isExcluded(key)) continue;
                
//                long size = memoryUsage.get(i) != null ? (long) memoryUsage.get(i) : 0;
                String redisKeyPrefix = getKeyPrefix(key);
                
                Map<String, Long> stats = data.computeIfAbsent(redisKeyPrefix, 
                    k -> new ConcurrentHashMap<>());
                
                stats.merge("keys", 1L, Long::sum);
//                stats.merge("memory", size, Long::sum);
            }
            
            long newTotal = totalKeys.addAndGet(keys.size());
            updateProgress(newTotal);
        } catch (Exception e) {
            logError("处理key内存使用情况出错", e);
        }
    }

    private void updateProgress(long processedKeys) {
        if (!headlessMode) {
            SwingUtilities.invokeLater(() -> {
                if (estimatedTotalKeys.get() > 0) {
                    int progress = (int) ((double) processedKeys / estimatedTotalKeys.get() * 100);
                    progressBar.setValue(Math.min(progress, 100));
                    progressBar.setString(String.format("%d%% (%,d/%,d)", 
                        progress, processedKeys, estimatedTotalKeys.get()));
                } else {
                    progressBar.setString(String.format("扫描中... (%,d keys)", processedKeys));
                }
            });
        }
    }

    private void scanDatabase(int db, String pattern) {
        String cursor = "0";
        int batchCount = 0;
        long startTime = System.currentTimeMillis();
        
        try {
            // First pass to estimate total keys
//            if (estimatedTotalKeys.get() == 0) {
//                logInfo("正在估算总key数量...");
//                ScanResult<String> estimateResult = RedisServiceUtil.scan(db, pattern, "0", 10000);
//                estimatedTotalKeys.set(estimateResult.getResult().size() * 100); // Rough estimate
//                logInfo(String.format("估算总key数量: %,d", estimatedTotalKeys.get()));
//            }

            do {
                long batchStartTime = System.currentTimeMillis();
                ScanResult<String> result = RedisServiceUtil.scan(db, pattern, cursor, currentScanCount);
                List<String> keys = result.getResult();
                
                if (!keys.isEmpty()) {
                    batchCount++;
                    executorService.submit(() -> processKeysBatch(db, keys));
                    
                    // Dynamic adjustment of scan count
                    if (batchCount % 5 == 0) {
                        long batchTime = System.currentTimeMillis() - batchStartTime;
                        if (batchTime > 1000 && currentScanCount > MIN_SCAN_COUNT) {
                            currentScanCount = Math.max(MIN_SCAN_COUNT, currentScanCount / 2);
                            logInfo(String.format("调整SCAN COUNT为 %d (响应时间 %.2fs)", 
                                currentScanCount, batchTime / 1000.0));
                        } else if (batchTime < 200 && currentScanCount < MAX_SCAN_COUNT) {
                            currentScanCount = Math.min(MAX_SCAN_COUNT, currentScanCount * 2);
                            logInfo(String.format("调整SCAN COUNT为 %d (响应时间 %.2fs)", 
                                currentScanCount, batchTime / 1000.0));
                        }
                    }
                    
                    if (batchCount % 10 == 0) {
                        logInfo(String.format("扫描进度: DB %d - 已处理 %,d 个key (当前批次: %,d)", 
                            db, totalKeys.get(), keys.size()));
                    }
                }
                
                cursor = result.getCursor();
            } while (!cursor.equals("0") && isRunning);
            
            long endTime = System.currentTimeMillis();
            logInfo(String.format("扫描完成 %s (DB %d): 总计 %,d 个key, 耗时 %.2f秒", 
                pattern, db, totalKeys.get(), (endTime - startTime) / 1000.0));
            
        } catch (JedisConnectionException e) {
            logError("Redis连接异常", e);
        } catch (Exception e) {
            logError("扫描出错", e);
        }
    }

    // Main analysis method
    public void analyzeRedisMemory() {
        if (isRunning) {
            logInfo("已有任务在运行，请等待完成");
            return;
        }

        if (!validateInputs()) {
            return;
        }

        isRunning = true;
        totalKeys.set(0);
        estimatedTotalKeys.set(0);
        data.clear();
        currentScanCount = 1000;

        try {
            String redisAddress = txtRedisAddress.getText();
            String redisPassword = new String(txtRedisPassword.getPassword());
            int selectedDb = (Integer) dbComboBox.getSelectedItem();

            logInfo("Redis内存分析开始 >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>");
            logInfo(String.format("连接Redis: %s (DB: %d)", redisAddress, selectedDb));
            
            String mode = modeComboBox.getSelectedItem().toString();
            
            RedisServiceUtil.init(mode, redisAddress, 
                redisPassword.isEmpty() ? null : redisPassword);
            
            // Update key patterns from input
            updateKeyPatterns();
            
            String prefix = txtKeyPrefix.getText().trim();
            String scanPattern = prefix.isEmpty() ? "*" : prefix + "*";
            
            scanDatabase(selectedDb, scanPattern);

            logInfo("所有分析完成，总计key数量: " + totalKeys.get());
            
            String results = formatResults(data);
            if (!headlessMode) {
                SwingUtilities.invokeLater(() -> {
                    txtResult.setText(results);
//                    showChart(data);
                });
            } else {
                System.out.println(results);
            }
            
        } catch (JedisConnectionException e) {
            logError("无法连接Redis", e);
        } catch (Exception e) {
            logError("分析过程中出错", e);
        } finally {
            isRunning = false;
            RedisServiceUtil.close();
            if (headlessMode) {
                executorService.shutdown();
                System.exit(0);
            }
        }
    }

    private void updateKeyPatterns() {
        String defaultKeysText = txtDefaultKeys.getText().trim();
        if (!defaultKeysText.isEmpty()) {
            redisKeys = defaultKeysText.split(",");
            for (int i = 0; i < redisKeys.length; i++) {
                redisKeys[i] = redisKeys[i].trim();
            }
        }
    }

    private boolean validateInputs() {
        if (txtRedisAddress.getText().trim().isEmpty()) {
            showError("Redis地址不能为空");
            return false;
        }
        
        String mode = modeComboBox.getSelectedItem().toString();
        if(mode.equals("single")) {
        	try {
                String[] parts = txtRedisAddress.getText().split(":");
                if (parts.length != 2) throw new IllegalArgumentException();
                Integer.parseInt(parts[1]);
            } catch (Exception e) {
                showError("Redis地址格式应为host:port");
                return false;
            }
        }else if(mode.equals("cluster")) {
        	try {
        		String redisAddress = txtRedisAddress.getText();
        		
        		if(redisAddress.indexOf(",")==-1) {
        			 showError("Redis地址格式应为host:port,host:port,host:port");
        			 return false;
        		}
        		
        		if(redisAddress.indexOf(":")==-1) {
       			 showError("Redis地址格式应为host:port,host:port,host:port");
       			 return false;
       		}
                
            } catch (Exception e) {
                showError("Redis地址格式应为host:port,host:port,host:port");
                return false;
            }
        }
        
        
        File savePath = new File(txtSavePath.getText());
        if (!savePath.exists() && !savePath.mkdirs()) {
            showError("无法创建保存目录");
            return false;
        }
        
        // Create logs directory if not exists
        new File(txtSavePath.getText() + "/logs").mkdirs();
        
        return true;
    }
    
    private void showError(String message) {
        if (!headlessMode) {
            JOptionPane.showMessageDialog(jf, message, "错误", JOptionPane.ERROR_MESSAGE);
        } else {
            System.err.println("错误: " + message);
        }
    }

    // Result formatting and display
    private String formatResults(Map<String, Map<String, Long>> data) {
        StringBuilder sb = new StringBuilder();
        sb.append("Redis内存统计结果:\n");
        sb.append("+--------------------------------+----------------+----------------+\n");
        sb.append("| Key Pattern                   | Key Count      | Memory Usage   |\n");
        sb.append("+--------------------------------+----------------+----------------+\n");
        
        long totalCount = 0;
        long totalMemory = 0;
        
        // Sort by memory usage descending
        List<Map.Entry<String, Map<String, Long>>> sortedEntries = new ArrayList<>(data.entrySet());
        sortedEntries.sort((e1, e2) -> 
            Long.compare(e2.getValue().getOrDefault("memory", 0L), 
                        e1.getValue().getOrDefault("memory", 0L)));
        
        for (Map.Entry<String, Map<String, Long>> entry : sortedEntries) {
            String keyPattern = entry.getKey();
            long count = entry.getValue().getOrDefault("keys", 0L);
            long memory = entry.getValue().getOrDefault("memory", 0L);
            
            sb.append(String.format("| %-30s | %,14d | %14s |\n", 
                keyPattern, count, formatMemory(memory)));
            
            totalCount += count;
            totalMemory += memory;
        }
        
        sb.append("+--------------------------------+----------------+----------------+\n");
        sb.append(String.format("| %-30s | %,14d | %14s |\n", 
            "TOTAL", totalCount, formatMemory(totalMemory)));
        sb.append("+--------------------------------+----------------+----------------+\n");
        
        sb.append("\n内存使用情况:\n");
        sb.append(String.format("总内存: %s\n", formatMemoryDetailed(totalMemory)));
        
        return sb.toString();
    }
    
    private String formatMemory(long bytes) {
        if (bytes >= 1024 * 1024) {
            return String.format("%.1f MB", bytes / (1024.0 * 1024.0));
        } else if (bytes >= 1024) {
            return String.format("%.1f KB", bytes / 1024.0);
        } else {
            return bytes + " B";
        }
    }
    
    private String formatMemoryDetailed(long bytes) {
        if (bytes >= 1024 * 1024 * 1024) {
            return String.format("%.2f GB (%,d bytes)", bytes / (1024.0 * 1024.0 * 1024.0), bytes);
        } else if (bytes >= 1024 * 1024) {
            return String.format("%.2f MB (%,d bytes)", bytes / (1024.0 * 1024.0), bytes);
        } else if (bytes >= 1024) {
            return String.format("%.2f KB (%,d bytes)", bytes / 1024.0, bytes);
        } else {
            return bytes + " bytes";
        }
    }
    
    private void showChart(Map<String, Map<String, Long>> data) {
        DefaultPieDataset dataset = new DefaultPieDataset();
        
        data.entrySet().stream()
            .sorted((e1, e2) -> Long.compare(
                e2.getValue().getOrDefault("memory", 0L),
                e1.getValue().getOrDefault("memory", 0L)))
            .limit(10) // Show top 10
            .forEach(entry -> {
                long memory = entry.getValue().getOrDefault("memory", 0L);
                if (memory > 0) {
                    dataset.setValue(entry.getKey(), memory);
                }
            });
        
        JFreeChart chart = ChartFactory.createPieChart(
            "Redis Key内存分布 (Top 10)", dataset, true, true, false);
        
        ChartFrame frame = new ChartFrame("统计图表", chart);
        frame.pack();
        frame.setVisible(true);
    }

    // GUI Initialization
    private static void createAndShowGUI() {
        jf = new JFrame("Redis内存分析工具 (专业版)");
        jf.setSize(1000, 800);
        jf.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
        jf.setLocationRelativeTo(null);

        JPanel panel = new JPanel();
        panel.setLayout(null);

        // Redis Connection Panel
        JPanel connectionPanel = new JPanel();
        connectionPanel.setBorder(BorderFactory.createTitledBorder("Redis连接设置"));
        connectionPanel.setBounds(10, 10, 970, 80);
        connectionPanel.setLayout(null);
        
        JLabel lblIp = new JLabel("Redis地址:");
        lblIp.setBounds(10, 20, 80, 25);
        connectionPanel.add(lblIp);

        txtRedisAddress = new JTextField("127.0.0.1:6379");
        txtRedisAddress.setBounds(90, 20, 150, 25);
        connectionPanel.add(txtRedisAddress);

        JLabel lblPort = new JLabel("密码:");
        lblPort.setBounds(250, 20, 50, 25);
        connectionPanel.add(lblPort);

        txtRedisPassword = new JPasswordField();
        txtRedisPassword.setBounds(290, 20, 100, 25);
        connectionPanel.add(txtRedisPassword);

        JLabel lblDb = new JLabel("数据库:");
        lblDb.setBounds(400, 20, 50, 25);
        connectionPanel.add(lblDb);

        dbComboBox = new JComboBox<>();
        for (int i = 0; i <= 16; i++) {
            dbComboBox.addItem(i);
        }
        dbComboBox.setBounds(450, 20, 60, 25);
        connectionPanel.add(dbComboBox);

        JLabel lblKeyPrefix = new JLabel("Key前缀:");
        lblKeyPrefix.setBounds(520, 20, 60, 25);
        connectionPanel.add(lblKeyPrefix);

        txtKeyPrefix = new JTextField("");
        txtKeyPrefix.setBounds(580, 20, 120, 25);
        connectionPanel.add(txtKeyPrefix);
        
        
        JLabel lbMode = new JLabel("运行模式:");
        lbMode.setBounds(720, 20, 60, 25);
        connectionPanel.add(lbMode);

        modeComboBox = new JComboBox<>();
         
        modeComboBox.addItem("single");
        modeComboBox.addItem("cluster");
        
        modeComboBox.setBounds(800, 20, 60, 25);
        connectionPanel.add(modeComboBox);

        panel.add(connectionPanel);

        // Key Patterns Panel
        JPanel keyPanel = new JPanel();
        keyPanel.setBorder(BorderFactory.createTitledBorder("Key模式设置"));
        keyPanel.setBounds(10, 100, 970, 60);
        keyPanel.setLayout(null);
        
        JLabel lblDefaultKeys = new JLabel("Key模式:");
        lblDefaultKeys.setBounds(10, 20, 80, 25);
        keyPanel.add(lblDefaultKeys);

        txtDefaultKeys = new JTextField(String.join(", ", DEFAULT_REDIS_KEYS));
        txtDefaultKeys.setBounds(90, 20, 850, 25);
        keyPanel.add(txtDefaultKeys);

        panel.add(keyPanel);
        
        

        // Save Path Panel
        JPanel pathPanel = new JPanel();
        pathPanel.setBorder(BorderFactory.createTitledBorder("保存路径"));
        pathPanel.setBounds(10, 170, 970, 50);
        pathPanel.setLayout(null);
        
        JLabel lblPath = new JLabel("保存路径:");
        lblPath.setBounds(10, 20, 80, 25);
        pathPanel.add(lblPath);

        txtSavePath = new JTextField("D:/data/redis_exp/");
        txtSavePath.setBounds(90, 20, 850, 25);
        pathPanel.add(txtSavePath);

        panel.add(pathPanel);

        // Log Panel
        JPanel logPanel = new JPanel();
        logPanel.setBorder(BorderFactory.createTitledBorder("日志"));
        logPanel.setBounds(10, 230, 970, 200);
        logPanel.setLayout(new BorderLayout());

        txtLog = new JTextArea();
        txtLog.setEditable(false);
        JScrollPane logScrollPane = new JScrollPane(txtLog);
        logPanel.add(logScrollPane);

        panel.add(logPanel);

        // Result Panel
        JPanel resultPanel = new JPanel();
        resultPanel.setBorder(BorderFactory.createTitledBorder("分析结果"));
        resultPanel.setBounds(10, 440, 970, 250);
        resultPanel.setLayout(new BorderLayout());

        txtResult = new JTextArea();
        txtResult.setEditable(false);
        JScrollPane resultScrollPane = new JScrollPane(txtResult);
        resultPanel.add(resultScrollPane);

        panel.add(resultPanel);

        // Progress Bar
        progressBar = new JProgressBar(0, 100);
        progressBar.setBounds(10, 700, 800, 20);
        progressBar.setStringPainted(true);
        panel.add(progressBar);

        // Control Buttons
        JButton btnStart = new JButton("开始分析");
        btnStart.setBounds(820, 700, 120, 25);
        btnStart.addActionListener(e -> new Thread(() -> {
            RedisMemoryStat stat = new RedisMemoryStat();
            stat.analyzeRedisMemory();
        }).start());
        panel.add(btnStart);

        JButton btnStop = new JButton("停止");
        btnStop.setBounds(820, 730, 120, 25);
        btnStop.addActionListener(e -> {
            isRunning = false;
            logInfo("用户手动停止扫描");
        });
        panel.add(btnStop);

        JButton btnChart = new JButton("显示图表");
        btnChart.setBounds(690, 730, 120, 25);
        btnChart.addActionListener(e -> {
            RedisMemoryStat stat = new RedisMemoryStat();
//            stat.showChart(stat.data);
        });
        panel.add(btnChart);

        jf.setContentPane(panel);
        jf.setVisible(true);
    }

    // Command line support
    public static void main(String[] args) {
        if (args.length > 0 && args[0].equalsIgnoreCase("--headless")) {
            headlessMode = true;
            initializeHeadlessMode();
            
            Map<String, String> params = parseArgs(args);
            RedisMemoryStat stat = new RedisMemoryStat();
            configureFromParams(stat, params);
            
            stat.analyzeRedisMemory();
        } else {
            headlessMode = false;
            SwingUtilities.invokeLater(RedisMemoryStat::createAndShowGUI);
        }
    }
    
    private static void initializeHeadlessMode() {
        txtRedisAddress = new JTextField("127.0.0.1:6379");
        txtRedisPassword = new JPasswordField();
        txtSavePath = new JTextField("D:/data/redis_exp/");
        txtDefaultKeys = new JTextField(String.join(", ", DEFAULT_REDIS_KEYS));
        dbComboBox = new JComboBox<>();
        for (int i = 0; i <= 16; i++) {
            dbComboBox.addItem(i);
        }
        
        // Create logs directory
        new File(txtSavePath.getText() + "/logs").mkdirs();
    }
    
    private static Map<String, String> parseArgs(String[] args) {
        Map<String, String> params = new HashMap<>();
        for (String arg : args) {
            if (arg.startsWith("--")) {
                String[] parts = arg.substring(2).split("=", 2);
                if (parts.length == 2) {
                    params.put(parts[0].toLowerCase(), parts[1]);
                }
            }
        }
        return params;
    }
    
    private static void configureFromParams(RedisMemoryStat stat, Map<String, String> params) {
        if (params.containsKey("host")) {
            txtRedisAddress.setText(params.get("host"));
        }
        if (params.containsKey("password")) {
            txtRedisPassword.setText(params.get("password"));
        }
        if (params.containsKey("db")) {
            dbComboBox.setSelectedItem(Integer.parseInt(params.get("db")));
        }
        if (params.containsKey("path")) {
            txtSavePath.setText(params.get("path"));
        }
        if (params.containsKey("defaultkeys")) {
            txtDefaultKeys.setText(params.get("defaultkeys"));
        }
        if (params.containsKey("prefix")) {
            txtKeyPrefix.setText(params.get("prefix"));
        }
        
        // Update key patterns
        String defaultKeysText = txtDefaultKeys.getText().trim();
        if (!defaultKeysText.isEmpty()) {
            stat.redisKeys = defaultKeysText.split(",");
            for (int i = 0; i < stat.redisKeys.length; i++) {
                stat.redisKeys[i] = stat.redisKeys[i].trim();
            }
        }
    }
}